/*
 * Copyright (C) 2010 Imavis Srl
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.imavis.avimuxer;

import java.io.File;
import java.io.IOException;

/**
 * Configures and creates an AVI muxer.
 * <p>
 * This class is used to set the parameters and configure the behaviour of an
 * AVI muxer. The muxer is created and started by calling the
 * {@link #open(java.io.File)} method.
 * <p>
 * Setter methods can be chained.
 * <p>
 * This class is immutable. All setter methods return a new
 * {@code AviMuxerBuilder} instance.
 * <p>
 * Currently supports only a single video stream.
 * 
 * @author Andrea Gualano
 */
public final class AviMuxerBuilder implements MuxerBuilder {
    private final int rate;
    private final int scale;
    private final ResolutionPolicy resolution;
    private final boolean avgRate;
    private final Encoding encoding;
    private final FourCC fourCC;
    private final boolean dropINKF;
    private final ParseContext parseContext;

    AviMuxerBuilder(int rate, int scale, ResolutionPolicy resolution,
            boolean avgRate, Encoding encoding, FourCC fourCC, boolean dropINKF,
            ParseContext parseContext) {
        this.rate = rate;
        this.scale = scale;
        this.resolution = resolution;
        this.avgRate = avgRate;
        this.encoding = encoding;
        this.fourCC = fourCC;
        this.dropINKF = dropINKF;
        this.parseContext = parseContext;
    }

    /**
     * Creates a new {@code AviMuxerBuilder} instance.
     * <p>
     * A few parameters must be set before a {@link Muxer} can be created.
     */
    public AviMuxerBuilder() {
        this(-1, -1, ResolutionPolicy.notSet(), false, Encoding.UNKNOWN, null,
                false,
                ParseContext.create(Encoding.UNKNOWN));
    }

    /**
     * Sets the frame rate for the video stream to a fixed value.
     *
     * @param rate frame rate numerator (in Hz)
     * @param scale frame rate denominator
     * @return a new {@code AviMuxerBuilder} instance.
     * @throws IllegalArgumentException if {@code rate} or {@code scale} are
     * not positive.
     */
    public AviMuxerBuilder framerate(int rate, int scale) {
        if (rate <= 0 || scale <= 0) {
            throw new IllegalArgumentException(
                    "Frame rate and scale must be positive");
        }
        return new AviMuxerBuilder(rate, scale, resolution, false,
                encoding, fourCC, dropINKF, parseContext);
    }

    /**
     * Calculate the frame rate for the video stream from the given timestamps.
     * <p>
     * If set to {@code true}, the muxer will set the video stream frame rate
     * to the average of frame timestamps, which must be non-{@code null}.
     *
     * @param avgRate if {@code true}, the frame rate will be calculated from
     * the given timestamps
     * @return a new {@code AviMuxerBuilder} instance.
     * @see Muxer#write(byte[], java.util.Date)
     */
    public AviMuxerBuilder averageFramerate(boolean avgRate) {
        return new AviMuxerBuilder(-1, -1, resolution, avgRate, encoding,
                fourCC, dropINKF, parseContext);
    }

    /**
     * Sets the resolution for the video stream.
     *
     * @param width horizontal resolution
     * @param height vertical resolution
     * @return a new {@code AviMuxerBuilder} instance.
     * @throws IllegalArgumentException if resolution is negative or too big.
     */
    public AviMuxerBuilder resolution(int width, int height) {
        if (width > Short.MAX_VALUE || height > Short.MAX_VALUE
                || 3L * width * height > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("Resolution is too big");
        }

        final ResolutionPolicy newResolution = ResolutionPolicy.fixed(width,
                height);
        return new AviMuxerBuilder(rate, scale, newResolution, avgRate,
                encoding, fourCC, dropINKF, parseContext);
    }

    /**
     * Configures the encoding used for the video stream.
     * <p>
     * By configuring an encoding, some muxer parameters will be set to
     * reasonable default values.
     *
     * @param encoding non-{@code null} {@link Encoding}
     * @return a new {@code AviMuxerBuilder} instance.
     * @throws NullPointerException if {@code encoding} is {@code null}.
     */
    public AviMuxerBuilder encoding(Encoding encoding) {
        if (encoding == null) {
            throw new NullPointerException("Encoding cannot be null");
        }

        final ParseContext newPc = ParseContext.create(encoding);

        final ResolutionPolicy newRes = (resolution == ResolutionPolicy.notSet())
                ? newPc.getResolution() : resolution;

        return new AviMuxerBuilder(rate, scale, newRes, avgRate,
                encoding, fourCC, dropINKF, newPc);
    }

    /**
     * Sets a four character code (4CC) for the video stream.
     * <p>
     * A 4CC is a four-character ASCII string used to identify the video codec
     * used to compress the video.
     *
     * @param fourCC non-{@code null} four-character string
     * @return a new {@code AviMuxerBuilder} instance.
     * @throws IllegalArgumentException if {@code fourCC} has the wrong length
     * or contains non-ASCII characters.
     * @throws NullPointerException if {@code fourCC} is {@code null}.
     */
    public AviMuxerBuilder fourCC(String fourCC) {
        return new AviMuxerBuilder(rate, scale, resolution, avgRate,
                encoding, new FourCC(fourCC), dropINKF,
                parseContext);
    }

    /**
     * Sets initial non-key frame policy.
     * <p>
     * If value is set to {@code true} and a parser is available for the
     * chosen encoding, the muxer will drop the frames before the first
     * key frame. The default behaviour is to write all frames to the
     * output file.
     *
     * @param dropINKF if {@code true}, enables initial frame dropping
     * @return a new {@code AviMuxerBuilder} instance.
     */
    public AviMuxerBuilder dropInitialNonKeyFrames(boolean dropINKF) {
        return new AviMuxerBuilder(rate, scale, resolution, avgRate,
                encoding, fourCC, dropINKF, parseContext);
    }

    void validate() throws IllegalStateException {
        if (rate == -1 && !avgRate) {
            throw new IllegalStateException("Frame rate must be set");
        }
        if (resolution == ResolutionPolicy.notSet()) {
            throw new IllegalStateException("Resolution must be set");
        }
        if (fourCC == null && encoding == Encoding.UNKNOWN) {
            throw new IllegalStateException("FourCC or encoding must be set");
        }
        if (dropINKF && encoding != Encoding.MPEG4) {
            throw new IllegalStateException(
                    "DropNonKeyFrames is implemented for MPEG4 only");
        }
    }

    @Override
    public Muxer open(File file) throws IllegalStateException, IOException {
        validate();
        int theRate = rate != -1 ? rate : 25;
        int theScale = scale != -1 ? scale : 1;
        FourCC theFourCC = fourCC != null ? fourCC : encoding.getFourCC();

        return new MasterMuxer(theRate, theScale, resolution, theFourCC,
                file, avgRate, dropINKF, parseContext.getParser());
    }

    // getters -- for testability only
    boolean isAvgRate() {
        return avgRate;
    }

    boolean isDropINKF() {
        return dropINKF;
    }

    Encoding getEncoding() {
        return encoding;
    }

    FourCC getFourCC() {
        return fourCC;
    }

    int getHeight() {
        return resolution.getHeight();
    }

    int getRate() {
        return rate;
    }

    int getScale() {
        return scale;
    }

    int getWidth() {
        return resolution.getWidth();
    }
}
